处理 ASP.NET Core Blazor 应用中的错误

您所在的位置:网站首页 blazor wasm 处理 ASP.NET Core Blazor 应用中的错误

处理 ASP.NET Core Blazor 应用中的错误

#处理 ASP.NET Core Blazor 应用中的错误 | 来源: 网络整理| 查看: 265

处理 ASP.NET Core Blazor 应用中的错误 项目 08/17/2023

注意

此版本不是本文的最新版本。 对于当前版本,请参阅本文的 .NET 7 版本。

重要事项

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅本文的 .NET 7 版本。

本文介绍 Blazor 如何管理未经处理的异常以及如何开发用于检测和处理错误的应用。

在本文中,术语客户端/客户端侧和服务器/服务器侧用于区分应用代码执行的位置:

客户端/客户端侧 Blazor Web 应用的客户端呈现 (CSR) 和交互性。 Program 文件是客户端项目(BlazorWeb-CSharp.Client)的 Program.cs。 带有 @page 指令的可路由组件放置在 Pages 文件夹中。 不可路由的共享组件通常放置在 Shared 文件夹中。 Blazor 脚本启动配置位于服务器项目(BlazorWeb-CSharp)的 App 组件(Components/App.razor)中。 Blazor WebAssembly 应用。 文件 Program 为 Program.cs. Blazor 脚本启动配置位于 wwwroot/index.html 文件中。 服务器/服务器端:Blazor Web 应用的服务器端呈现 (SSR) 和交互性。 Program 文件是服务器项目(BlazorWeb-CSharp)的 Program.cs。 带有 @page 指令的可路由组件放置在 Components/Pages 文件夹中。 不可路由的共享组件通常放置在 Components 文件夹中。 Blazor 脚本启动配置位于 App 组件(Components/App.razor)中。 客户端/客户端侧 托管的 Blazor WebAssembly 应用的 Client 项目。 Blazor WebAssembly 应用。 Blazor 脚本启动配置位于 wwwroot/index.html 文件中。 文件 Program 为 Program.cs. 服务器/服务器端 托管的 Blazor WebAssembly 应用的 Server 项目。 Blazor Server 应用。 Blazor 脚本启动配置位于 Pages/_Host.cshtml 中。 文件 Program 为 Program.cs. 客户端/客户端侧 托管的 Blazor WebAssembly 应用的 Client 项目。 Blazor WebAssembly 应用。 Blazor 脚本启动配置位于 wwwroot/index.html 文件中。 文件 Program 为 Program.cs. 服务器/服务器端 托管的 Blazor WebAssembly 应用的 Server 项目。 Blazor Server 应用。 Blazor 脚本启动配置位于 Pages/_Layout.cshtml 中。 文件 Program 为 Program.cs. 客户端/客户端侧 托管的 Blazor WebAssembly 应用的 Client 项目。 Blazor WebAssembly 应用。 Blazor 脚本启动配置位于 wwwroot/index.html 文件中。 文件 Program 为 Program.cs. 服务器/服务器端 托管的 Blazor WebAssembly 应用的 Server 项目。 Blazor Server 应用。 Blazor 脚本启动配置位于 Pages/_Host.cshtml 中。 文件 Program 为 Program.cs. 开发过程中的错误详细信息

当 Blazor 应用在开发过程中运行不正常时,从该应用接收详细的错误信息有助于故障排除和修复问题。 出现错误时,Blazor 应用会在屏幕底部显示一个浅黄色条框:

在开发过程中,这个条框会将你定向到浏览器控制台,你可在其中查看异常。 在生产过程中,这个条框会通知用户发生了错误,并建议刷新浏览器。

此错误处理体验的 UI 属于 Blazor 项目模板。

在 Blazor Web 应用中,在 MainLayout 组件中自定义体验。 由于 Razor 组件中不支持环境标记帮助程序(例如,...),因此以下示例注入 IHostEnvironment 以配置不同环境的错误消息。

在 Components/Layout/MainLayout.razor 的顶部:

@inject IHostEnvironment HostEnvironment

创建或修改 Blazor 错误 UI 标记:

@if (HostEnvironment.IsProduction()) { An error has occurred. } else { An unhandled exception occurred. } Reload 🗙

在 Blazor Server 应用中,在 Pages/_Host.cshtml 文件中自定义体验。 以下示例使用环境标记帮助程序为不同的环境配置错误消息。

在 Blazor Server 应用中,在 Pages/_Layout.cshtml 文件中自定义体验。 以下示例使用环境标记帮助程序为不同的环境配置错误消息。

在 Blazor Server 应用中,在 Pages/_Host.cshtml 文件中自定义体验。 以下示例使用环境标记帮助程序为不同的环境配置错误消息。

创建或修改 Blazor 错误 UI 标记:

An error has occurred. An unhandled exception occurred. Reload 🗙

在独立 Blazor WebAssembly 应用中,在 wwwroot/index.html 文件中自定义体验:

An unhandled error has occurred. Reload 🗙

blazor-error-ui 元素通常是隐藏的,因为应用的自动生成样式表中存在 blazor-error-ui CSS 类的 display: none 样式。 当发生错误时,框架将 display: block 应用于该元素。

由于 wwwroot/css 文件夹中站点样式表中存在 blazor-error-ui CSS 类的 display: none 样式,blazor-error-ui 元素通常被隐藏。 当发生错误时,框架将 display: block 应用于该元素。

在 Razor 组件的生命周期外处理捕获的异常

在 Razor 组件中使用 ComponentBase.DispatchExceptionAsync 来处理在组件的生命周期调用堆栈外部引发的异常。 这允许组件的代码将异常视为生命周期方法异常。 此后,Blazor 的错误处理机制(如错误边界)可以处理异常。

注意

ComponentBase.DispatchExceptionAsync 用于继承自 ComponentBase 的 Razor 组件文件 (.razor)。 创建实现 implement IComponent directly 的组件时,请使用 RenderHandle.DispatchExceptionAsync。

若要处理在 Razor 组件生命周期之外捕获的异常,请将异常传递给 DispatchExceptionAsync,然后等待结果:

try { ... } catch (Exception ex) { await DispatchExceptionAsync(ex); }

一种常见情况是,组件想要启动异步操作,但未等待 Task。 如果操作失败,你可能仍希望组件将失败视为以下示例目标的组件生命周期异常:

例如,将组件置于出错状态,以触发错误边界。 如果没有错误边界,则终止线路。 触发针对生命周期异常发生的相同日志记录。

在以下示例中,用户选择“发送报表”按钮,以触发发送报表的后台方法 ReportSender.SendAsync。 在大多数情况下,组件会等待异步调用的 Task,并更新 UI 以指示操作已完成。 在以下示例中,SendReport 方法不会等待 Task,也不会向用户报告结果。 由于组件有意放弃 SendReport 中的 Task,因此任何异步故障都发生在正常的生命周期调用堆栈中,因而不会被 Blazor 看到:

Send report @code { private void SendReport() { _ = ReportSender.SendAsync(); } }

若要处理生命周期方法异常等故障,请使用 DispatchExceptionAsync 将异常显式调度回组件,如以下示例所示:

Send report @code { private void SendReport() { _ = SendReportAsync(); } private async Task SendReportAsync() { try { await ReportSender.SendAsync(); } catch (Exception ex) { await DispatchExceptionAsync(ex); } } }

对于 DispatchExceptionAsync 的工作演示,请在外部调用组件方法以更新状态中实现计时器通知示例。 在 Blazor 应用中,从计时器通知示例中添加以下文件,并在 Program 文件中注册服务,如以下部分所述:

TimerService.cs NotifierService.cs ReceiveNotifications.razor

该示例使用 Razor 组件生命周期之外的计时器,其中未处理的异常通常不会由 Blazor 的错误处理机制(如错误边界)处理。

首先,更改 TimerService.cs 中的代码,以在组件的生命周期之外创建人工异常。 在 TimerService.cs 的 while 循环中,当 elapsedCount 达到 2 的值时引发异常:

if (elapsedCount == 2) { throw new Exception("I threw an exception! Somebody help me!"); }

在应用的主布局中放置错误边界。 将 ... 标记替换为以下标记。

Components/Layout/MainLayout.razor:

@Body

如果此时运行应用,当经过的计数达到 2 的值时,将引发异常。 但是,UI 不会更改。 错误边界不显示错误内容。

更改 ReceiveNotification 组件的 OnNotify 方法 (ReceiveNotification.razor):

在 try-catch 块中包装对 ComponentBase.InvokeAsync 的调用。 将任意 Exception 传递给 DispatchExceptionAsync 并等待结果。 public async Task OnNotify(string key, int value) { try { await InvokeAsync(() => { lastNotification = (key, value); StateHasChanged(); }); } catch (Exception ex) { await DispatchExceptionAsync(ex); } }

当计时器服务执行并达到 2 的计数时,异常将调度到 Razor 组件,然后反过来触发错误边界,以在 MainLayout 组件中显示 的错误内容:

Oh, dear! Oh, my! - George Takei

详细线路错误

本部分适用于通过线路运行的 Blazor Web 应用。

本部分适用于 Blazor Server 应用。

客户端错误不包括调用堆栈,也不提供有关错误原因的详细信息,但服务器日志的确包含此类信息。 出于开发目的,可通过启用详细错误向客户端提供敏感线路错误信息。

将 CircuitOptions.DetailedErrors 设置为 true。 有关详细信息和示例,请参阅 ASP.NET Core BlazorSignalR 指南。

另一种设置 CircuitOptions.DetailedErrors 的方法是在应用开发环境设置文件 (appsettings.Development.json) 中将 DetailedErrors 配制键设置为 true。 此外,SignalR 服务器端日志记录 (Microsoft.AspNetCore.SignalR) 可设置为调试或跟踪,以用于详细 SignalR 日志记录。

appsettings.Development.json:

{ "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", "Microsoft.AspNetCore.SignalR": "Debug" } } }

在开发/过渡环境服务器或本地系统上,还可以使用 ASPNETCORE_DETAILEDERRORS 环境变量和值 true 将 DetailedErrors 配置键设置为 true。

警告

始终避免向 Internet 上的客户端公开错误信息,这会带来安全风险。

在开发人员代码中管理未经处理的异常

若要在出现错误后继续运行应用,该应用必须具备错误处理逻辑。 本文后面的部分将介绍未经处理的异常出现的潜在原因。

在生产环境中,不要在 UI 中呈现框架异常消息或堆栈跟踪信息。 呈现异常消息或堆栈跟踪信息可能导致:

向最终用户公开敏感信息。 帮助恶意用户发现应用中可能会危及应用、服务器或网络安全的弱点。 线路的未经处理的异常

本部分适用于通过线路运行的 Blazor Web 应用。

本部分适用于 Blazor Server 应用。

启用了服务器交互的 Razor 组件在服务器上是有状态的。 当用户与服务器上的组件交互时,他们会保持与服务器的连接,称为线路。 线路包含活动组件实例,以及状态的许多其他方面,例如:

最新呈现的组件输出。 可由客户端事件触发的事件处理委托的当前集合。

如果用户在多个浏览器标签页中打开应用,则用户就会创建多条独立线路。

Blazor 将大部分未经处理的异常视为发生该异常的线路的严重异常。 如果线路由于未经处理的异常而终止,则用户只能重新加载页面来创建新线路,从而继续与应用进行交互。 终止的线路以外的其他线路(即其他用户或其他浏览器标签页的线路)不会受到影响。 此场景类似于出现故障的桌面应用。 出现故障的应用必须重新启动,但其他应用不受影响。

当发生未处理异常时,框架会出于以下原因终止线路:

未经处理的异常通常会将线路置于未定义状态。 发生未经处理的异常后,应用可能无法正常运行。 如果线路继续保持未定义状态,应用中可能会出现安全漏洞。 全局异常处理

有关全局异常处理,请参阅以下部分:

错误边界 备用全局异常处理 错误边界

错误边界提供了一种用于处理异常的便捷方法。 ErrorBoundary 组件:

在未发生错误时呈现其子内容。 在引发未处理的异常时呈现错误 UI。

要定义错误边界,请使用 ErrorBoundary 组件来包装现有内容。 例如,可以在应用主布局的正文内容周围添加错误边界。

在 MainLayout.razor中:

@Body

应用继续正常运行,但错误边界会处理未处理的异常。

请考虑使用以下示例,其中 Counter 组件在计数增加超过 5 时引发异常。

在 Counter.razor中:

private void IncrementCount() { currentCount++; if (currentCount > 5) { throw new InvalidOperationException("Current count is too big!"); } }

如果 currentCount 超过 5,则会引发未处理的异常:

错误记录正常 (System.InvalidOperationException: Current count is too big!)。 异常由错误边界处理。 错误 UI 由错误边界呈现,并显示以下默认错误消息:An error has occurred.

默认情况下,ErrorBoundary 组件会为其错误内容呈现具有 blazor-error-boundary CSS 类的空 元素。 默认 UI 的颜色、文本和图标是使用 wwwroot 文件夹中应用样式表中的 CSS 定义的,因此可以自定义错误 UI。

通过设置 ErrorContent 属性来更改默认错误内容:

@Body

😈 A rotten gremlin got us. Sorry!

由于错误边界是在前面的示例中在布局中定义的,因此无论用户在错误发生后导航到哪个页面,都会看到错误 UI。 建议在大多数场景下缩小错误边界的范围。 如果设置了较广泛的错误边界,则可以通过调用错误边界的 Recover 方法,在后续页面导航事件中将其重置为非错误状态。

在 MainLayout.razor中:

为 ErrorBoundary 添加字段,以使用 @ref 属性指令捕获对其的引用。 在 OnParameterSet 生命周期方法中,使用 Recover 在错误边界上触发恢复。 ... @Body ... @code { private ErrorBoundary? errorBoundary; protected override void OnParametersSet() { errorBoundary?.Recover(); } }

为了避免无限循环,其中恢复只会重新呈现再次引发错误的组件,请勿从呈现逻辑调用 Recover。 仅在以下情况下呼叫 Recover:

用户执行 UI 手势,例如选择按钮以指示其想要重试过程,或者当用户导航到新组件时。 额外的逻辑也会清除异常。 重新呈现组件时,错误不会再次出现。 备用全局异常处理

可将自定义错误组件作为 CascadingValue 传递给子组件,来代替使用错误边界 (ErrorBoundary)。 与使用注入式服务或自定义记录器实现相比,使用组件的一个优点是,在发生错误时,级联组件可以呈现内容并应用 CSS 样式。

下面的 Error 组件示例仅记录错误,但组件的方法可以按照应用要求的任何方式处理错误,包括通过使用多种错误处理方法。

Components/Error.razor:

Error.razor:

@using Microsoft.Extensions.Logging @inject ILogger Logger @ChildContent @code { [Parameter] public RenderFragment? ChildContent { get; set; } public void ProcessError(Exception ex) { Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", ex.GetType(), ex.Message); } }

注意

有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件。

在 App 组件中,用 Error 组件将 Routes 组件包装起来。 这允许 Error 组件向下级联到应用中将 Error 组件作为 CascadingParameter 接收的任何组件。

在 App.razor中:

在 App 组件中,用 Error 组件将 Router 组件包装起来。 这允许 Error 组件向下级联到应用中将 Error 组件作为 CascadingParameter 接收的任何组件。

在 App.razor中:

...

要在组件中处理错误:

将 Error 组件指定为 @code 块中的 CascadingParameter。 在基于 Blazor 项目模板的应用的示例 Counter 组件中,添加以下 Error 属性:

[CascadingParameter] public Error? Error { get; set; }

使用适当的异常类型在任何 catch 块中调用错误处理方法。 该示例 Error 组件只提供了一个 ProcessError 方法,但错误处理组件可以提供任意数量的错误处理方法来解决整个应用中的其他错误处理要求。 在下面的 Counter 组件示例中,当计数大于 5 时会引发并捕获异常:

@code { private int currentCount = 0; [CascadingParameter] public Error? Error { get; set; } private void IncrementCount() { try { currentCount++; if (currentCount > 5) { throw new InvalidOperationException("Current count is over five!"); } } catch (Exception ex) { Error?.ProcessError(ex); } } }

使用前面的 Error 组件和先前对 Counter 组件做出的更改,浏览器的开发人员工具控制台会指示捕获并记录的错误:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.InvalidOperationException Message: Current count is over five!

如果 ProcessError 方法直接参与呈现,例如,显示自定义错误消息栏或更改所呈现元素的 CSS 样式,请在 ProcessErrors 方法末尾调用 StateHasChanged 来重新呈现 UI。

由于本部分中的方法使用 try-catch 语句处理错误,因此在发生错误时,客户端和服务器之间应用的 SignalR 连接不会中断,并且线路保持活跃状态。 其他未处理异常对于线路来说仍然是严重异常。 有关详细信息,请参阅有关线路如何对未经处理的异常作出反应的部分。

应用可以使用错误处理组件作为级联值来集中处理错误。

以下 Error 组件将自身作为 CascadingValue 传递给子组件。 下面的示例仅记录错误,但组件的方法可以按照应用要求的任何方式处理错误,包括通过使用多种错误处理方法。 与使用注入式服务或自定义记录器实现相比,使用组件的一个优点是,在发生错误时,级联组件可以呈现内容并应用 CSS 样式。

Error.razor:

@using Microsoft.Extensions.Logging @inject ILogger Logger @ChildContent @code { [Parameter] public RenderFragment ChildContent { get; set; } public void ProcessError(Exception ex) { Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", ex.GetType(), ex.Message); } }

注意

有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件。

在 App 组件中,用 Error 组件将 Router 组件包装起来。 这允许 Error 组件向下级联到应用中将 Error 组件作为 CascadingParameter 接收的任何组件。

App.razor:

...

要在组件中处理错误:

将 Error 组件指定为 @code 块中的 CascadingParameter:

[CascadingParameter] public Error Error { get; set; }

使用适当的异常类型在任何 catch 块中调用错误处理方法。 该示例 Error 组件只提供了一个 ProcessError 方法,但错误处理组件可以提供任意数量的错误处理方法来解决整个应用中的其他错误处理要求。

try { ... } catch (Exception ex) { Error.ProcessError(ex); }

使用前面的示例 Error 组件和 ProcessError 方法,浏览器的开发人员工具控制台会指示捕获并记录的错误:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

如果 ProcessError 方法直接参与呈现,例如,显示自定义错误消息栏或更改所呈现元素的 CSS 样式,请在 ProcessErrors 方法末尾调用 StateHasChanged 来重新呈现 UI。

由于本部分中的方法使用 try-catch 语句处理错误,因此在发生错误时,客户端和服务器之间的 Blazor 应用的 SignalR 连接不会中断,并且线路保持活跃状态。 任何未处理异常对于线路而言都是致命错误。 有关详细信息,请参阅有关线路如何对未经处理的异常作出反应的部分。

使用永久性提供程序记录错误信息

在发生未经处理的异常时,将异常记录到在服务容器中配置的 ILogger 实例。 默认情况下,Blazor 应用使用控制台日志记录提供程序记录到控制台输出中。 请考虑使用管理日志大小和日志轮换的提供程序登录到服务器上的某个位置(或客户端应用的后端 Web API)。 或者,应用也可以使用应用程序性能管理 (APM) 服务,如 Azure Application Insights (Azure Monitor)。

说明

用于支持客户端应用的本机 Application Insights 功能和对 Google Analytics 的本机 Blazor 框架支持可能会在这些技术的未来版本中推出。 有关更多信息,请参阅在 Blazor WASM 客户端支持 App Insights (microsoft/ApplicationInsights-dotnet #2143) 和 Web 分析和诊断(包含社区实现链接)(dotnet/aspnetcore #5461)。 同时,客户端应用可以结合使用 Application Insights JavaScript SDK 和 JS 互操作直接将错误从客户端应用记录到 Application Insights。

在线路上运行的 Blazor 应用中开发时,应用通常会将异常的完整详细信息发送到浏览器的控制台来帮助进行调试。 在生产环境中,详细错误不会发送到客户端,但异常的完整详细信息会记录在服务器上。

必须确定要记录的事件以及已记录的事件的严重性级别。 恶意用户也许能刻意触发错误。 例如,若显示产品详细信息的组件的 URL 中提供了未知的 ProductId,则请勿记录错误中的事件。 并非所有的错误都应被视为需要记录的事件。

有关详细信息,请参阅以下文章:

ASP.NET Core Blazor 日志记录 处理 ASP.NET Core 中的错误‡ 使用 ASP.NET Core 创建 Web API

‡适用于 Blazor 服务器端应用和其他服务器端 ASP.NET Core 应用,这些应用是 Blazor 的 Web API 后端应用。 客户端应用可捕获客户端上的错误信息,并将其发送到 Web API,该 API 将错误信息记录到持久日志记录提供程序。

在发生未经处理的异常时,将异常记录到在服务容器中配置的 ILogger 实例。 默认情况下,Blazor 应用使用控制台日志记录提供程序记录到控制台输出中。 考虑将日志记录到服务器上保存时间更长久的位置,方法是将错误信息发送到后端 Web API,并且该 API 使用具有日志大小管理和日志轮替功能的日志记录提供程序。 或者,后端 Web API 应用可使用应用程序性能管理 (APM) 服务(如 Azure Application Insights (Azure Monitor)†)来记录从客户端接收的错误信息。

必须确定要记录的事件以及已记录的事件的严重性级别。 恶意用户也许能刻意触发错误。 例如,若显示产品详细信息的组件的 URL 中提供了未知的 ProductId,则请勿记录错误中的事件。 并非所有的错误都应被视为需要记录的事件。

有关详细信息,请参阅以下文章:

ASP.NET Core Blazor 日志记录 处理 ASP.NET Core 中的错误‡ 使用 ASP.NET Core 创建 Web API

†用于支持客户端应用的本机 Application Insights 功能和对 Google Analytics 的本机 Blazor 框架支持可能会在这些技术的未来版本中推出。 有关更多信息,请参阅在 Blazor WASM 客户端支持 App Insights (microsoft/ApplicationInsights-dotnet #2143) 和 Web 分析和诊断(包含社区实现链接)(dotnet/aspnetcore #5461)。 同时,客户端应用可以结合使用 Application Insights JavaScript SDK 和 JS 互操作直接将错误从客户端应用记录到 Application Insights。

‡适用于服务器端 ASP.NET Core 应用,这些应用是 Blazor 应用的 Web API 后端应用。 客户端应用捕获错误信息,并发送到 Web API,该 API 将错误信息记录到持久日志记录提供程序。

可能发生错误的位置

框架和应用代码可能会在以下任何位置触发未处理异常,相关的具体内容将在本文后续部分介绍:

组件实例化 生命周期方法 呈现逻辑 事件处理程序 组件处置 JavaScript 互操作 预呈现 组件实例化 生命周期方法 呈现逻辑 事件处理程序 组件处置 JavaScript 互操作 预呈现 组件实例化

当 Blazor 创建某组件的实例时:

会调用该组件的构造函数。 会调用通过 @inject 指令或 [Inject] 特性提供给组件构造函数的 DI 服务的构造函数。

已执行构造函数中或任何 [Inject] 属性的 setter 中发生错误,可能会引起未处理异常,并阻止框架对组件进行实例化。 如果应用在线路上运行,则线路出现故障。 如果构造函数逻辑可能引发异常,应用应使用 try-catch 语句捕获异常,并进行错误处理和日志记录。

生命周期方法

在组件的生命周期内,Blazor 会调用 生命周期方法 。 如果任何生命周期方法以同步或异步方式引发异常,则该异常对于 线路而言是严重异常。 若要使组件处理生命周期方法中的错误,请添加错误处理逻辑。

在下面的示例中,OnParametersSetAsync 会调用方法来获取产品:

ProductRepository.GetProductByIdAsync 方法中引发的异常由 try-catch 语句处理。 在执行 catch 块时: loadFailed 设置为 true,用于向用户显示一条错误消息。 错误会被记录。 @page "/product-details/{ProductId:int}" @using Microsoft.Extensions.Logging @inject ILogger Logger @inject IProductRepository ProductRepository @if (details != null) { @details.ProductName

@details.Description

} else if (loadFailed) { Sorry, we could not load this product due to an error. } else { Loading... } @code { private ProductDetail? details; private bool loadFailed; [Parameter] public int ProductId { get; set; } protected override async Task OnParametersSetAsync() { try { loadFailed = false; details = await ProductRepository.GetProductByIdAsync(ProductId); } catch (Exception ex) { loadFailed = true; Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId); } } public class ProductDetail { public string? ProductName { get; set; } public string? Description { get; set; } } public interface IProductRepository { public Task GetProductByIdAsync(int id); } } @page "/product-details/{ProductId:int}" @using Microsoft.Extensions.Logging @inject ILogger Logger @inject IProductRepository ProductRepository @if (details != null) { @details.ProductName

@details.Description

} else if (loadFailed) { Sorry, we could not load this product due to an error. } else { Loading... } @code { private ProductDetail? details; private bool loadFailed; [Parameter] public int ProductId { get; set; } protected override async Task OnParametersSetAsync() { try { loadFailed = false; details = await ProductRepository.GetProductByIdAsync(ProductId); } catch (Exception ex) { loadFailed = true; Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId); } } public class ProductDetail { public string? ProductName { get; set; } public string? Description { get; set; } } public interface IProductRepository { public Task GetProductByIdAsync(int id); } } @page "/product-details/{ProductId:int}" @using Microsoft.Extensions.Logging @inject ILogger Logger @inject IProductRepository ProductRepository @if (details != null) { @details.ProductName

@details.Description

} else if (loadFailed) { Sorry, we could not load this product due to an error. } else { Loading... } @code { private ProductDetail details; private bool loadFailed; [Parameter] public int ProductId { get; set; } protected override async Task OnParametersSetAsync() { try { loadFailed = false; details = await ProductRepository.GetProductByIdAsync(ProductId); } catch (Exception ex) { loadFailed = true; Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId); } } public class ProductDetail { public string ProductName { get; set; } public string Description { get; set; } } public interface IProductRepository { public Task GetProductByIdAsync(int id); } } @page "/product-details/{ProductId:int}" @using Microsoft.Extensions.Logging @inject ILogger Logger @inject IProductRepository ProductRepository @if (details != null) { @details.ProductName

@details.Description

} else if (loadFailed) { Sorry, we could not load this product due to an error. } else { Loading... } @code { private ProductDetail details; private bool loadFailed; [Parameter] public int ProductId { get; set; } protected override async Task OnParametersSetAsync() { try { loadFailed = false; details = await ProductRepository.GetProductByIdAsync(ProductId); } catch (Exception ex) { loadFailed = true; Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId); } } public class ProductDetail { public string ProductName { get; set; } public string Description { get; set; } } public interface IProductRepository { public Task GetProductByIdAsync(int id); } } 呈现逻辑

Razor 组件文件 (.razor) 中的声明性标记被编译到名为 BuildRenderTree 的 C# 方法中。 当组件呈现时,BuildRenderTree 会执行并构建一个数据结构,该结构描述所呈现组件的元素、文本和子组件。

呈现逻辑可能会引发异常。 例如评估了 @someObject.PropertyName,但 @someObject 为 null 时,就会发生这种情况。 对于在线路上运行的 Blazor 应用,呈现逻辑引发的未经处理的异常对于应用的线路来说是严重异常。

为防止呈现逻辑中出现 NullReferenceException,请在访问其成员之前检查是否存在 null 对象。 在以下示例中,如果 person.Address 为 null,则不访问 person.Address 属性:

@if (person.Address != null) { @person.Address.Line1 @person.Address.Line2 @person.Address.City @person.Address.Country }

上述代码假定 person 不是 null。 通常,代码的结构保证了呈现组件时存在对象。 在这些情况下,不需要检查呈现逻辑中是否存在 null。 在前面的示例中,由于在实例化组件时创建了 person,因此可保证存在 person,如以下示例所示:

@code { private Person person = new(); ... } 事件处理程序

使用以下内容创建事件处理程序时,客户端代码将触发 C# 代码调用:

@onclick @onchange 其他 @on... 特性 @bind

在这些情况下,事件处理程序代码可能会引发未经处理的异常。

如果应用调用可能因外部原因而失败的代码,请使用 try-catch 语句捕获异常,并进行错误处理和日志记录。

如果事件处理程序引发未经处理的异常(例如数据库查询失败),其未捕获且由开发人员代码处理:

框架会记录异常。 在线路上运行的 Blazor 应用中,异常对于应用的线路来说是严重异常。 组件处置

例如,可从 UI 中删除组件,因为用户已导航到其他页面。 当从 UI 中删除实现 System.IDisposable 的组件时,框架将调用该组件的 Dispose 方法。

如果组件的 Dispose 方法在线路上运行的 Blazor 应用中引发未经处理的异常,则该异常对于应用的线路来说是严重异常。

如果处置逻辑可能引发异常,应用应使用 try-catch 语句捕获异常,并进行错误处理和日志记录。

有关组件处置的详细信息,请参阅 ASP.NET Core Razor 组件生命周期。

JavaScript 互操作

IJSRuntime 由 Blazor 框架注册。 IJSRuntime.InvokeAsync 允许 .NET 代码在用户浏览器中对 JavaScript (JS) 运行时进行异步调用。

以下条件适用于带有 InvokeAsync 的错误处理:

如果无法对 InvokeAsync 进行同步调用,则会发生 .NET 异常。 例如,对 InvokeAsync 的调用可能会失败,因为不能序列化提供的自变量。 开发人员代码必须捕获异常。 如果事件处理程序或组件生命周期方法中的应用代码在线路上运行的 Blazor 应用中没有处理异常,则该异常对于应用的线路来说是严重异常。 如果无法对 InvokeAsync 进行异步调用,则 .NET Task 会失败。 例如,对 InvokeAsync 的调用可能会失败,这是因为 JS 端代码会引发异常或返回完成状态为 rejected 的 Promise。 开发人员代码必须捕获异常。 如果使用 await 运算符,请考虑使用 try-catch 语句包装方法调用,并进行错误处理和日志记录。 否则,在线路上运行的 Blazor 应用中,失败的代码会导致未经处理的异常,这对于应用的线路来说是严重异常。 默认情况下,对 InvokeAsync 的调用必须在特定时间段内完成,否则调用会超时。默认超时时间为一分钟。 超时会保护代码免受网络连接丢失的影响,或者保护永远不会发回完成消息的 JS 代码。 如果调用超时,则生成的 System.Threading.Tasks 将失败,并出现 OperationCanceledException。 捕获异常,并进行异常处理和日志记录。

同样,JS 代码可以对 [JSInvokable] 特性指示的 .NET 方法发起调用。 如果这些 .NET 方法引发未经处理的异常:

在线路上运行的 Blazor 应用中,异常不会被视为对应用线路严重异常。 JS 端 Promise 将被拒绝。

可选择在方法调用的 .NET 端或 JS 端使用错误处理代码。

有关详细信息,请参阅以下文章:

在 ASP.NET Core Blazor 中从 .NET 方法调用 JavaScript 函数 在 ASP.NET Core Blazor 中从 JavaScript 函数调用 .NET 方法 预呈现

默认情况下,Razor 组件是预先呈现的,因此它们呈现的 HTML 标记将作为用户初始 HTTP 请求的一部分返回。

在线路上运行的 Blazor 应用中,预呈现的工作方式如下:

为属于同一页面的所有预呈现组件创建新的线路。 生成初始 HTML。 将线路视为 disconnected,直到用户浏览器与同一服务器重新建立起 SignalR 连接。 建立该连接后,将恢复线路的交互性,并更新组件的 HTML 标记。

对于预提交的客户端组件,预提交的工作方式如下:

针对属于同一页的所有预呈现组件,在服务器上生成初始 HTML。 在浏览器加载应用的已编译代码和 .NET 运行时后(如果尚未加载),使组件在客户端上交互。

如果组件在预呈现期间引发未经处理的异常,例如在生命周期方法或呈现逻辑中:

在线路上运行的 Blazor 应用中,异常对于线路来说是严重异常。 对于预呈现的客户端组件,异常会阻止呈现组件。 此异常将从 ComponentTagHelper 中的调用堆栈引发。

在正常情况下,如果预呈现失败,则继续生成和呈现组件都将没有作用,因为无法呈现工作组件。

若要容许在预呈现期间可能发生的错误,必须将错误处理逻辑置于可能引发异常的组件中。 请使用 try-catch 语句,并进行错误处理和日志记录。 请勿将 ComponentTagHelper 包装在 try-catch 语句中,而是将错误处理逻辑放在由 ComponentTagHelper 呈现的组件中。

高级方案 递归呈现

组件能以递归方式嵌套。 这适用于表示递归数据结构。 例如,TreeNode 组件可以为节点的每个子级呈现更多 TreeNode 组件。

以递归方式呈现时,请避免采用会导致无限递归的编码模式:

请勿以递归方式呈现包含循环的数据结构。 例如,请勿呈现其子级包含其自身的树节点。 请勿创建包含循环的布局链。 例如,请勿创建布局为其本身的布局。 请勿允许最终用户通过恶意数据输入或 JavaScript 互操作调用违反递归固定协定(规则)。

呈现过程中的无限循环:

会导致呈现过程永久地继续下去。 相当于创建不终止的循环。

在这些情况下,Blazor 会失败,通常会尝试:

在操作系统允许范围内无限期地消耗 CPU 时间。 消耗不限量的内存。 消耗不限量的内存相当于不终止的循环在每次迭代时向集合添加条目的情况。

若要避免无限递归模式,请确保递归呈现代码包含合适的停止条件。

自定义呈现器树逻辑

大多数 Razor 组件都实现为 Razor 组件文件 (.razor),并由框架编译以生成在 RenderTreeBuilder 上运行的逻辑,从而呈现其输出。 但是,开发人员可使用程序 C# 代码手动实现 RenderTreeBuilder 逻辑。 有关详细信息,请参阅 ASP.NET Core Blazor 高级方案(呈现器树构造)。

警告

手动呈现树生成器逻辑被视为一种高级且不安全的方案,不建议开发人员在常规组件开发工作中采用。

如果编写 RenderTreeBuilder 代码,开发人员必须保证代码的正确性。 例如,开发人员必须确保:

对 OpenElement 和 CloseElement 的调用已正确均衡。 仅将特性添加到正确的位置。

如果手动呈现树生成器逻辑不正确,可能会出现任意未定义的行为(包括崩溃、应用或服务器挂起)以及安全漏洞。

请知悉:手动呈现树生成器逻辑的复杂程度和危险程度与手动编写程序集代码或 Microsoft 中间语言 (MSIL) 指令是一样的。

其他资源 ASP.NET Core Blazor 日志记录 处理 ASP.NET Core 中的错误† 使用 ASP.NET Core 创建 Web API Blazor 示例 GitHub 存储库 (dotnet/blazor-samples)

†适用于客户端 Blazor 应用用于日志记录的后端 ASP.NET Core Web API 应用。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3